package com.eii.resubmit.aop.request.advisor;

import com.eii.resubmit.aop.request.UAOP;
import com.eii.resubmit.aop.request.UServlet;
import com.eii.resubmit.aop.request.annotation.RepeatLock;
import com.eii.resubmit.aop.request.checker.ManualChecker;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 参考：https://blog.csdn.net/kristoph/article/details/85100257
 *
 * @author eii
 * @sice 2020/11/28
 */
@Slf4j
@Component
public class RepeatAdvisor implements PointcutAdvisor {

    /**
     * 阻塞型重复请求检查器
     */
    @Qualifier("manualGuavaChecker")
    @Autowired
    private ManualChecker checker;

    //接口调用计数
    private static ConcurrentHashMap<String, AtomicInteger> countMap = new ConcurrentHashMap<>();

    /**
     * 执行器
     * 保证TIMEOUT（60s）时间内，同一个ip，请求一个接一个的执行，并发执行将显示"error:重复请求"
     */
    private Advice advice = (MethodInterceptor) invocation -> {
//        RepeatLock annotation = invocation.getMethod().getAnnotation(RepeatLock.class);
//        RepeatLock annotation = AnnotationUtils.findAnnotation(invocation.getMethod(),RepeatLock.class)
//        System.out.println("advice annotation:" + annotation);
        HttpServletRequest request = UServlet.request();
        String key = Util.servletKey(Util.fullName(invocation.getMethod()), request);
        AtomicInteger atomicInteger = countMap.computeIfAbsent(key, (k) -> new AtomicInteger(0));
        log.info("advice:{},调用计数:{}", key, atomicInteger.addAndGet(1));
        try {
            if (!checker.lock(key)) {
                log.info("重复请求:IP地址{}", UServlet.getAddr(request));
                Map<String, String> resMap = new HashMap<>();
                resMap.put("result", "error:重复请求:IP地址[" + UServlet.getAddr(request) + "]");
                return resMap;
            }
            return invocation.proceed();
        } finally {
            log.info("checker.free 调用计数:{}", atomicInteger.get());
            checker.free(key);
        }
    };
    /**
     * 匹配器
     * 执行顺序
     * 1、先执行两个参数的matches()，
     * 2、1中返回true则执行isRuntime()，1中返回false则中止
     * 3、isRuntime()返回true则执行三个参数的matches()，isRuntime()返回false则中止
     * 4、特别注意上面三步只是是执行流程，但是需要重点关注两个参数的matches()，其结果true或false决定了advice是否执行
     * <p>
     * MethodMatcher接口通过重载定义了两个matches()方法，
     * 两个参数的matches()被称为静态匹配，在匹配条件不是太严格时使用，可以满足大部分场景的使用，
     * 称之为静态的主要是区分为三个参数的matches()方法需要在运行时动态的对参数的类型进行匹配；
     * 两个方法的分界线就是boolean isRuntime()方法，进行匹配时先用两个参数的matches()方法进行匹配，
     * 若匹配成功，则检查boolean isRuntime()的返回值，若为true，则调用三个参数的matches()方法进行匹配（若两个参数的都匹配不中，三个参数的必定匹配不中），
     * 比如需要统计用户登录次数时，那么登录传入的参数就是可以忽略的，则调用两个参数的matches()方法就已经足够了，
     * 但是若要在登陆时对用户账号执行特殊的操作（如赋予特殊的操作权限），就需要对参数进行一个类似于检验的操作，就需要调用三个参数的matches()进行匹配。
     */
    //对每个代理类进行判断，只判断一次
    private MethodMatcher methodMatcher = new MethodMatcher() {

        //step1： 这里对方法级别进行排除，不需要此advice的方法，可从这这里判断
        @Override
        public boolean matches(Method method, Class<?> targetClass) {
            log.info("两个参数的matches()，其结果true或false决定了advice是否执行");
            log.info("targetClass:{}, matchesMethod:{}", targetClass.getName(), method.getName());
            if (!UAOP.isController(targetClass)) {
                return false;
            }
            RepeatLock annotation = RepeatLock.RepeatLockHelper.get(method, targetClass);
            if (annotation == null) {
                return false;
            }
            return true;
        }

        //step2：
        @Override
        public boolean isRuntime() {
            log.info("isRuntime 返回false，不需要进行三个参数的matches()的执行");
            return false;
        }

        //step3：
        @Override
        public boolean matches(Method method, Class<?> aClass, Object... objects) {
            log.info("matches 3 args");
            return false;
        }
    };

    private Pointcut pointcut = new Pointcut() {

        @Override
        public MethodMatcher getMethodMatcher() {
            return methodMatcher;
        }

        //这里对类进行级别排除，不需要此advice的类，可从这这里判断
        @Override
        public ClassFilter getClassFilter() {
            return clazz -> true;
        }

    };


    @Override
    public Pointcut getPointcut() {
        return pointcut;
    }

    @Override
    public Advice getAdvice() {
        return advice;
    }

    //step0：Spring还没有使用该属性 永远返回true了
    @Override
    public boolean isPerInstance() {
        log.info("isPerInstance");
        return true;
    }
}
/*

Pointcut接口作为SpringAop中对AOP的最顶层抽象，主要负责对系统的相应的Joinpoint进行捕捉，并且提供了一个TruePointcut实例，当Pointcut为TruePointcut类型时，则会忽略所有的匹配条件，对系统中所有的对象进行Joinpoint所定义的规则进行匹配；

ClassFilter与MethodMatcher分别用于在不同的级别上限定Joinpoint的匹配范围，满足不同粒度的匹配，ClassFilter限定在类级别上，MethodMatcher限定在方法级别上；但是SpringAop主要支持在方法级别上的匹配，所以对类级别的匹配支持相对简单一些；
当传入的clazz与Pointcut规定的类型一致时，则返回true，否则返回false,返回为true时，则表示对这个类进行植入操作，当类型对Joinpoint的匹配不产生影响的时候，可以让Pointcut接口中的ClassFilter getClassFilter()方法直接返回TrueClassFilter.INSTANCE,则表示对系统中的所有对象进行Joinpoint匹配；

MethodMatcher接口通过重载定义了两个matches()方法，两个参数的matches()被称为静态匹配，在匹配条件不是太严格时使用，可以满足大部分场景的使用，称之为静态的主要是区分为三个参数的matches()方法需要在运行时动态的对参数的类型进行匹配；两个方法的分界线就是boolean isRuntime()方法，进行匹配时先用两个参数的matches()方法进行匹配，若匹配成功，则检查boolean isRuntime()的返回值，若为true，则调用三个参数的matches()方法进行匹配（若两个参数的都匹配不中，三个参数的必定匹配不中），比如需要统计用户登录次数时，那么登录传入的参数就是可以忽略的，则调用两个参数的matches()方法就已经足够了，但是若要在登陆时对用户账号执行特殊的操作（如赋予特殊的操作权限），就需要对参数进行一个类似于检验的操作，就需要调用三个参数的matches()进行匹配。
 */
